#!/usr/bin/env python

__author__ = "Miki Tebeka <miki.tebeka@gmail.com>"
__licence__ = "BSD"
__version__ = "0.0.2"

# Discovery based test suite for JavaScript
# Use either command line (SpiderMonkey or Rhino) or browser
# Test files are in the format test_*.js or given explicitly in the command line

# TODO
# * Option to run in browser (generate HTML)
# * Option to ignore directories / files
# * Option to change test file pattern / test function pattern
# * setup/teardown functions
# * Option to include JS libraries (jquery, prototype ...)
# * Can we get return value from the browser?
# * Currently in browser mode you can't have two functions in the same name
#   tested

import re
from os import walk, environ, pathsep
from os.path import join, abspath, isfile
from subprocess import call, Popen
from fnmatch import fnmatch
from sys import path
from tempfile import gettempdir
import webbrowser

def which(app):
    if isfile(app):
        return app

    for dirname in environ.get("PATH", "").split(pathsep):
        app_path = join(dirname, app)
        if isfile(app_path):
            return app_path

    return ""

_find_tests1 = re.compile("function\s+(test_\w+)", re.M).findall
_find_tests2 = re.compile("(test_\w+)\s+=\s+function", re.M).findall

def find_tests(filename):
    data = open(filename).read()
    return _find_tests1(data) + _find_tests2(data);

TEST_TEMPLATE = '''
function assert(condition, message) {
    if (condition) {
        return;
    }

    throw message;
}

function run_test_suite(tests, names) {
    var num_errors = 0;

    for (var i = 0; i < tests.length; ++i) {
        print("* Running " + names[i] + "...");
        try {
            tests[i]();
            print("* OK");
        }
        catch (error) {
            print("* ERROR (" + error + ")");
            ++num_errors;
        }
    }
    quit(num_errors);
}
load('%(testfile)s');

run_test_suite(
[%(tests)s],
[%(names)s]
);
'''

HTML_SUITE_TEMPLATE = '''
{
    filename : '%(filename)s',
    tests : [%(tests)s],
    names : [%(names)s]
}
'''

HTML_PAGE_TEMPLATE = '''
<html>
    <head>
        <title>SmokeJS</title>
        <style>
            .ok {
                color: blue;
            }
            .error {
                color: red;
            }
        </style>
        <script>

            function log(message) {
                var pre = document.getElementById("log");
                pre.innerHTML = pre.innerHTML + message + "\\n";
            }

            function assert(condition, message) {
                if (condition) {
                    return;
                }

                throw message;
            }

            function run_test_suite(tests, names) {
                var num_errors = 0;

                for (var i = 0; i < tests.length; ++i) {
                    log("* Running " + names[i] + "...");
                    try {
                        tests[i]();
                        log("* <span class='ok'>OK</span>");
                    }
                    catch (error) {
                        log("* <span class='error'>ERROR</span> (" + error + ")");
                        ++num_errors;
                    }
                }

                return num_errors;

            }

            function run_suites(test_suites) {
                var num_failed = 0;
                var num_tests = 0;
                for (var i = 0; i < test_suites.length; ++i) {
                    var suite = test_suites[i];
                    log("** Testing " + suite.filename + " (" + 
                        suite.tests.length + " tests)");
                    var nfailed = run_test_suite(suite.tests, suite.names);
                    if (nfailed == 0) {
                        log("** All tests passed");
                    }
                    else {
                        log("** " + nfailed + " tests failed");
                    }
                    log("");

                    num_failed = num_failed + nfailed;
                    num_tests += suite.tests.length;
                }

                var status = document.getElementById("status");
                var message = (num_failed == 0) ? 
                    "<span class='ok'>PASSED</span>" : 
                    "<span class='error'>FAILED</span>";

                status.innerHTML = message;
                log("");
                log("*** " + message); 
            }

            %(tests)s

            function initialize() {

                run_suites([
                %(suites)s
                ]);
            }

            window.onload = initialize;
        </script>
    </head>
    <body>
        <h1>SmokeJS</h1>
        <b>Status:</b> <span id="status">Runnig ...</span> <br />
        <b>Log:</b> <br />
        <pre id="log"></pre>
    </body>
</html>
'''

def js_names(tests):
    return ", ".join(("'%s'" % test for test in tests))

def js_tests(tests):
    return ", ".join(tests)

def test_file(filename, tests, js="js"):
    if not tests:
        return 0, 0

    env = {
        "testfile" : filename,
        "tests" : js_tests(tests),
        # We use names since js don't always get the function name right
        "names" : js_names(tests),
    }

    testfile = join(gettempdir(), "testsuite.js")
    fo = open(testfile, "w")
    print >> fo, TEST_TEMPLATE % env
    fo.close()

    num_failed = call("%s %s" % (js, testfile), shell=1)

    return len(tests), num_failed

def find_files(root_dir):
    for root, dirs, files in walk(root_dir):
        for filename in files:
            if fnmatch(filename, "test_*.js"):
                yield join(root, filename)

def commandline_test(test_suites, js="js"):
    num_tests = num_failed = 0
    for filename, tests in test_suites:
        print "** Testing %s" % filename
        ntests, nfailed = test_file(filename, tests, js=js)
        if ntests + nfailed == 0:
            print "* Warning - can't find any tests to run"
            continue

        if nfailed == 0:
            print "** All %d tests passed" % ntests
        else:
            print "** %d/%d tests failed" % (nfailed, ntests)
        num_tests += ntests
        num_failed = nfailed
        print

    print
    if num_failed == 0:
        print "*** DONE: All %d tests passed" % num_tests
    else:
        print "*** DONE: Total of %d/%d tests failed" % (num_failed, num_tests)
        raise SystemExit(1)

def guess_js():
    for name in ["js", "rhino"]:
        js = which(name)
        if js:
            return js
    return ""

def gen_html(test_suites):
    jstests = []
    code = []
    for filename, tests in test_suites:
        env = {
            "filename" : filename,
            "names" : js_names(tests),
            "tests" : js_tests(tests),
        }
        jstests.append(HTML_SUITE_TEMPLATE % env)
        code.append(open(filename).read())

    env = {
        "suites" : ",\n".join(jstests),
        "tests" : "\n".join(code),
    }

    return HTML_PAGE_TEMPLATE % env

def browser_test(test_suites, browser):
    html = gen_html(test_suites)
    filename = join(gettempdir(), "smokejs.html")
    fo = open(filename, "wt")
    fo.write(html)
    fo.close()

    if browser:
        Popen([browser, filename])
    else:
        webbrowser.open(filename)

def main(argv=None):
    if argv is None:
        import sys
        argv = sys.argv

    from optparse import OptionParser
    from os.path import isfile

    parser = OptionParser("usage: %prog [options] [FILE(s)]",
                          version=__version__)
    parser.add_option("-j", "--js", help="path to JavaScript executable",
                      dest="js", default="")
    parser.add_option("-b", help="run in browser", dest="use_browser",
                      action="store_true", default=0)
    parser.add_option("--browser", 
                      help="specify which browser to run (implies -b)", 
                      dest="browser", action="store", default="")


    opts, args = parser.parse_args(argv[1:])
    if len(args) not in (0, 1):
        parser.error("wrong number of arguments") # Will exit

    if args:
        files_to_test = args
        for filename in files_to_test:
            if not isfile(filename):
                raise SystemExit("error: can't find %s" % filename)
    else:
        files_to_test = list(find_files("."))

    if not files_to_test:
        raise SystemExit("error: can't find any files to test")

    suites = map(lambda filename: (filename, find_tests(filename)),
                files_to_test)

    if opts.use_browser or opts.browser:
        if opts.browser and not which(opts.browser):
            raise SystemExit("error: can't find %s" % opts.browser)
        browser_test(suites, opts.browser)
        raise SystemExit


    # If we're here, it's command line
    if opts.js:
        if not which(opts.js):
            raise SystemExit("error: can't find %s" % opts.js)
    else:
        opts.js = guess_js()
        if not isfile(opts.js):
            raise SystemExit("error: can't find JavaScript executable")


    commandline_test(suites, opts.js)

if __name__ == "__main__":
    main()
